/*
 * Copyright 2013-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.cloud.nacos.client;

import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.NacosPropertySourceRepository;
import com.alibaba.cloud.nacos.parser.NacosDataParserHandler;
import com.alibaba.cloud.nacos.refresh.NacosContextRefresher;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.yaml.snakeyaml.Yaml;

import java.util.*;
import java.util.stream.Collectors;

import static net.ibizsys.central.cloud.core.spring.configuration.NacosServiceHubSettingBase.DATAID_DBINST_PREFIX;

/**
 * 加载nacos中配置文件及配置项，并存储至环境变量中
 * 注：当配置文件存在相同属性时，将通过配置文件名+配置项名称方式合并配置，如：deploySystem-plm.yml 、deploySystem-gateway.yml 中均存在 runtimeclasses 配置项，
 * 则环境变量中会存入 deploySystem-plm.runtimeclasses =1 , deploySystem-gateway.runtimeclasses =2
 */
@Getter
@Setter
@Order(0)
public class BootPropertySourceLocator extends NacosPropertySourceLocator {

	private static final Logger log = LoggerFactory
			.getLogger(BootPropertySourceLocator.class);

	private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS";

	private static final String SEP1 = "-";

	private static final String DOT = ".";

	private NacosPropertySourceBuilder nacosPropertySourceBuilder;

	private NacosConfigProperties nacosConfigProperties;

	private NacosConfigManager nacosConfigManager;

	private String fileName;

	private String fileExtension;

	public BootPropertySourceLocator(NacosConfigManager nacosConfigManager) {
		super(nacosConfigManager);
		this.nacosConfigManager = nacosConfigManager;
		this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
	}

	@Override
	public PropertySource<?> locate(Environment env) {
		nacosConfigProperties.setEnvironment(env);
		ConfigService configService = nacosConfigManager.getConfigService();

		if (null == configService) {
			log.warn("no instance of config service found, can't load config from nacos");
			return null;
		}
		long timeout = nacosConfigProperties.getTimeout();
		nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
				timeout);
		String name = nacosConfigProperties.getName();

		String dataIdPrefix = nacosConfigProperties.getPrefix();
		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = name;
		}

		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = env.getProperty("spring.application.name");
		}

		CompositePropertySource composite = new CompositePropertySource(
				NACOS_PROPERTY_SOURCE_NAME);

		loadSharedConfiguration(composite);
		loadExtConfiguration(composite);
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		return composite;
	}

	/**
	 * load shared configuration.
	 */
	private void loadSharedConfiguration(
			CompositePropertySource compositePropertySource) {
		List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
				.getSharedConfigs();
		if (!CollectionUtils.isEmpty(sharedConfigs)) {
			checkConfiguration(sharedConfigs, "shared-configs");
			loadNacosConfiguration(compositePropertySource, sharedConfigs);
		}
	}

	/**
	 * load extensional configuration.
	 */
	private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
		List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
				.getExtensionConfigs();
		if (!CollectionUtils.isEmpty(extConfigs)) {
			checkConfiguration(extConfigs, "extension-configs");
			loadExtNacosConfiguration(compositePropertySource, extConfigs);
			mergeNacosConfiguration(compositePropertySource, extConfigs);
		}
	}

	/**
	 * 合并nacos配置，将系统扩展配置合并到主配置文件中（deploysystem-ex.yml -> deploysystem.yml）
	 * @param compositePropertySource
	 */
	private void mergeNacosConfiguration(CompositePropertySource compositePropertySource, List<NacosConfigProperties.Config> extConfigs) {

		if (!ObjectUtils.isEmpty(compositePropertySource.getPropertySources())) {
			for (PropertySource propertySource : compositePropertySource.getPropertySources()) {
				if (!(propertySource instanceof NacosPropertySource))
					continue;

				NacosPropertySource nacosPropertySource = (NacosPropertySource) propertySource;
				if (!ObjectUtils.isEmpty(nacosPropertySource.getDataId())) {
					Map<String, Object> propertySourceMap = nacosPropertySource.getSource();
					if (ObjectUtils.isEmpty(propertySourceMap))
						continue;

					Map<String, Object> propertySourceMap2 = new LinkedHashMap<>();
					for (Map.Entry<String, Object> entry : propertySourceMap.entrySet()) {
						propertySourceMap2.put(String.format("%1$s.%2$s", getDataId(nacosPropertySource), entry.getKey()), entry.getValue());
					}
					propertySourceMap.clear();
					propertySourceMap.putAll(propertySourceMap2);
					//servicehub 中的 deploysystem 节点处理
					setDeploySystem(nacosPropertySource);
				}
			}
		}
	}

	/**
	 * 将servicehub中的deploysystem配置设置到环境变量
	 * @param nacosPropertySource
	 */
	private void setDeploySystem(NacosPropertySource nacosPropertySource) {

		try {
			Map<String, Object> propertySourceMap = nacosPropertySource.getSource();
			String configId = nacosPropertySource.getDataId();

			if (!ObjectUtils.isEmpty(configId) && configId.startsWith("servicehub-")) {
				String deploySystemId = String.format("%1$s.deploysystems", getDataId(nacosPropertySource));
				if (propertySourceMap.keySet().stream().anyMatch(key -> key.startsWith(deploySystemId))){
					//获取部署系统nacos配置
					ConfigService configService = nacosConfigManager.getConfigService();
					String configValue = configService.getConfig(configId, nacosConfigProperties.getGroup(), 5000);
					if (ObjectUtils.isEmpty(configValue)) {
						log.info(String.format("未定义远程配置[%1$s]", configId));
						return;
					}
					Yaml yaml = new Yaml();
					Map config = yaml.loadAs(configValue, Map.class);
					if (!ObjectUtils.isEmpty(config) && config.containsKey("deploysystems")) {
						propertySourceMap.put(String.format(String.format("%1$s.deploysystems", getDataId(nacosPropertySource))), config.get("deploysystems"));
					}
				}
			}
		} catch (NacosException e) {
			log.error(String.format("解析[%1$s]配置发生异常，未能将部署系统远程配置设置到环境变量", getDataId(nacosPropertySource)));
			e.printStackTrace();
		}
	}

	/**
	 * 获取主配置文件的dataId：若当前是扩展配置文件时，则返回主配置文件的dataId
	 * @param nacosPropertySource
	 * @return
	 */
	private String getDataId(NacosPropertySource nacosPropertySource){

		String dataId = nacosPropertySource.getDataId();
		if(ObjectUtils.isEmpty(dataId))
			return dataId;

		if(dataId.startsWith(DATAID_DBINST_PREFIX) && dataId.endsWith("-ex"))
			dataId = dataId.replace("-ex","");

		return dataId;
	}

	/**
	 * load configuration of application.
	 */
	private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) {
		String fileExtension = properties.getFileExtension();
		String nacosGroup = properties.getGroup();
		// load directly once by default
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
				fileExtension, true);
		// load with suffix, which have a higher priority than the default
		loadNacosDataIfPresent(compositePropertySource,
				dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		// Loaded with profile, which have a higher priority than the suffix
		for (String profile : environment.getActiveProfiles()) {
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
					fileExtension, true);
		}

	}


	private void loadNacosConfiguration(final CompositePropertySource composite,
										List<NacosConfigProperties.Config> configs) {
		for (NacosConfigProperties.Config config : configs) {
			loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
					NacosDataParserHandler.getInstance()
							.getFileExtension(config.getDataId()),
					config.isRefresh());
		}
	}


	private void loadExtNacosConfiguration(final CompositePropertySource composite,
										   List<NacosConfigProperties.Config> configs) {
		for (NacosConfigProperties.Config config : configs) {
			loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
					setDefaultFileExtension(config) ? this.getFileExtension() : NacosDataParserHandler.getInstance().getFileExtension(config.getDataId()),
					config.isRefresh());
		}
	}

	/**
	 * 是否获取默认配置文件名
	 *
	 * @param config
	 * @return
	 */
	private boolean setDefaultFileExtension(NacosConfigProperties.Config config) {

		//默认将所有扩展文件使用默认后缀
		if (!ObjectUtils.isEmpty(fileExtension) && ObjectUtils.isEmpty(fileName))
			return true;

		//指定配置文件使用默认后缀
		if (!ObjectUtils.isEmpty(fileExtension) && !ObjectUtils.isEmpty(fileName) && Arrays.asList(fileName.split(",|;")).contains(config.getDataId()))
			return true;

		return false;
	}

	private void checkConfiguration(List<NacosConfigProperties.Config> configs,
			String tips) {
		for (int i = 0; i < configs.size(); i++) {
			String dataId = configs.get(i).getDataId();
			if (dataId == null || dataId.trim().length() == 0) {
				throw new IllegalStateException(String.format(
						"the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId",
						tips, i));
			}
		}
	}

	private void loadNacosDataIfPresent(final CompositePropertySource composite,
			final String dataId, final String group, String fileExtension,
			boolean isRefreshable) {
		if (null == dataId || dataId.trim().length() < 1) {
			return;
		}
		if (null == group || group.trim().length() < 1) {
			return;
		}
		NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
				fileExtension, isRefreshable);

		this.addFirstPropertySource(composite, propertySource, false);
	}

	private NacosPropertySource loadNacosPropertySource(final String dataId,
			final String group, String fileExtension, boolean isRefreshable) {
		if (NacosContextRefresher.getRefreshCount() != 0) {
			if (!isRefreshable) {
				return NacosPropertySourceRepository.getNacosPropertySource(dataId,
						group);
			}
		}
		return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
				isRefreshable);
	}

	/**
	 * Add the nacos configuration to the first place and maybe ignore the empty
	 * configuration.
	 */
	private void addFirstPropertySource(final CompositePropertySource composite,
			NacosPropertySource nacosPropertySource, boolean ignoreEmpty) {
		if (null == nacosPropertySource || null == composite) {
			return;
		}
		if (ignoreEmpty && nacosPropertySource.getSource().isEmpty()) {
			return;
		}
		composite.addFirstPropertySource(nacosPropertySource);
	}

	public void setNacosConfigManager(NacosConfigManager nacosConfigManager) {
		this.nacosConfigManager = nacosConfigManager;
	}

}
